using System;
using System.Collections.Generic;
using System.Reflection;
using RikMigrations.Providers;

namespace RikMigrations
{
	public class MigrationManager
	{
		public const string DefaultModuleName = "DEFAULT";

		internal static void Initialise(DbProvider db)
		{
			if (!db.TableExists("ModuleVersions"))
			{
			    Console.WriteLine("Initialising RikMigrations");
				Table table = db.AddTable("ModuleVersions");
				table.AddColumn("ModuleName", typeof(string), 64).PrimaryKey().NotNull();
				table.AddColumn("CurrentVersion", typeof(int)).Default(0).NotNull();
				table.Save();
			}
		}

		public static void MigrateTo(Assembly assembly, string module, int version)
		{
			MigrateTo(assembly, module, version, new MssqlProvider());
		}

		public static void MigrateTo(Assembly assembly, string module, int version, DbProvider db)
		{
		    Console.WriteLine(String.Format("Migrating to module {0} version {1} using {2}", module, version,
                                            db.ConnectionString));
			if (module == null)
				module = DefaultModuleName;

			Initialise(db);

			List<MigrationAttribute> migrations = FindAllMigrations(assembly, module)[module];
			int currentVersion = db.GetModuleVersion(module);
			int sourceIndex = migrations.FindIndex(delegate(MigrationAttribute a) {return a.Version == currentVersion;});
			int targetIndex;
			if (version < currentVersion)
				targetIndex = migrations.FindIndex(delegate(MigrationAttribute a) { return a.Version == version + 1; });
			else
				targetIndex = migrations.FindIndex(delegate(MigrationAttribute a) {return a.Version == version;});
			if (targetIndex == -1)
				throw new ArgumentException("Unable to migrate to that version");
			
			if (sourceIndex < targetIndex)
			{
				/*if (sourceIndex == -1)
					sourceIndex = 0; // Initial migration...*/
				for (int i = sourceIndex+1; i <= targetIndex; i++)
					migrations[i].Up(db);
			}
			else if (targetIndex <= sourceIndex)
			{
				for (int i = sourceIndex; i >= targetIndex; i--)
					migrations[i].Down(db);
			}
		}

		public static void UpgradeMax(Assembly assembly)
		{
			UpgradeMax(assembly, new MssqlProvider());
		}

		public static void UpgradeMax(Assembly assembly, DbProvider db, params string[] moduleNames)
		{
			Initialise(db);

			Dictionary<string, List<MigrationAttribute>> migrations = FindAllMigrations(assembly, moduleNames);

			foreach (string module in migrations.Keys)
			{
                Console.WriteLine(String.Format("Migrating to latest version of module {0} using {1}", module, DbProvider.DefaultConnectionString));

				int currentVersion = db.GetModuleVersion(module);
				foreach (MigrationAttribute attr in migrations[module])
				{
					if (attr.Version == currentVersion + 1)
					{
						attr.Up(db);
						currentVersion++;
					}
				}
			}
		}

		/// <summary>
		/// Gets all migrations in the assembly, keyed by module name and ordered by version
		/// </summary>
		/// <returns></returns>
		public static Dictionary<string, List<MigrationAttribute>> FindAllMigrations(Assembly assembly, params string[] moduleNames)
		{
			Dictionary<string, List<MigrationAttribute>> retVal = new Dictionary<string, List<MigrationAttribute>>();

            addMigrationToList(moduleNames, retVal, (MigrationAttribute[])assembly.GetCustomAttributes(typeof(MigrationAttribute), false), null);

            foreach (Type t in assembly.GetExportedTypes())
            {
                    addMigrationToList(moduleNames, retVal, ( MigrationAttribute[])t.GetCustomAttributes(typeof(MigrationAttribute), false), t);
            }

			return retVal;
		}

	    private static void addMigrationToList(string[] moduleNames, Dictionary<string, List<MigrationAttribute>> retVal, MigrationAttribute[] migrationsFound, Type t)
	    {
            foreach (MigrationAttribute migration in migrationsFound)
            {
                string moduleName = migration.ModuleName ?? DefaultModuleName;

                if (t != null)
                    migration.Type = t;

                if (moduleNames == null || moduleNames.Length == 0 || Contains(moduleNames, moduleName))
                {
                    List<MigrationAttribute> migrations;
                    if (!retVal.TryGetValue(moduleName, out migrations))
                    {
                        migrations = new List<MigrationAttribute>();
                        retVal.Add(moduleName, migrations);
                    }

                    int index = migrations.BinarySearch(migration);

                    if (index < 0)
                        migrations.Insert(~index, migration);
                }
            }
	    }

	    static bool Contains(string[] array, string value)
		{
			foreach (string s in array)
				if (s == value)
					return true;
			return false;
		}
	}
}
